/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Id: Utils.java 468 2007-02-15 20:59:10Z jack.rybicki $
 */
package logmatcher;

import org.apache.commons.beanutils.MethodUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.ConversionException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.UnhandledException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.beans.PropertyDescriptor;

import java.io.FileNotFoundException;
import java.io.PrintWriter;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import java.util.Iterator;


/**
 * Utility class. Initializes objects according to configuration.
 *
 * @version $Revision: 468 $
*/
public final class Utils {
    //~ Static fields/initializers -----------------------------------------------------------------------------------

    /** Class logger. */
    private static final Log LOGGER = LogFactory.getLog(Utils.class);

    //~ Constructors -------------------------------------------------------------------------------------------------

/**
     * Private constructor - this class cannot be instantiated.
     *
     */
    private Utils() {
        super();
    }

    //~ Methods ------------------------------------------------------------------------------------------------------

    /**
     * Instantiates new object of the class specified by name.
     *
     * @param className name of class to be instantiated.
     *
     * @return new Object of desired class.
     *
     * @throws InitializationException in case of any error occurs.
     */
    public static Object loadObject(String className) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Trying to load: " + className);
        }

        try {
            Class c = Class.forName(className);

            return c.newInstance();
        } catch (ClassNotFoundException e) {
            throw new InitializationException("Invalid class name", e);
        } catch (InstantiationException e) {
            throw new InitializationException("Invalid constructor", e);
        } catch (IllegalAccessException e) {
            throw new InitializationException("No access to the class", e);
        }
    }


    /**
     * Initailize object of expected type according to configuration content.
     *
     * @param config Configuration object.
     * @param prefix identification of object configuration elements.
     * @param expectedType class of initialized object, used for additional verification.
     * @param register if this object is to be registered in configuration.
     *
     * @return initialized object.
     *
     * @throws InitializationException in case of any error while object initialization.
     */
    public static Object initElement(Configuration config, String prefix, Class expectedType, boolean register) {
        String className = config.getString(prefix + ".class");

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(
                "Trying to load " + expectedType + " identified by " + prefix + ", loading class " + className);
        }

        Object element = Utils.loadObject(className);

        if (!expectedType.isInstance(element)) {
            throw new InitializationException(
                "Class " + element.getClass() + " do not implement/extend" + expectedType);
        }

        if (element instanceof Configurable) {
            ((Configurable)element).configure(config, prefix);
        }

        config.addProperty(prefix, element);

        return element;
    }


    /**
     * Initialize list of objects of the same type according to configuration.
     *
     * @param config Configuration object.
     * @param prefix identification of list of objects.
     * @param expectedType class of initialized object, used for additional verification.
     * @param register if initialized objects are to be registered in configuration.
     *
     * @return array of newly initized objects.
     *
     * @throws InitializationException if list of objects to initialize is empty or any other exception is thrown
     *         while object initialization.
     */
    public static Object[] initListOfElements(
        Configuration config, String prefix, Class expectedType, boolean register) {
        String[] elementNames = config.getStringArray(prefix);

        if (elementNames.length == 0) {
            throw new InitializationException("Empty list of " + expectedType);
        }

        Object[] elements = new Object[elementNames.length];

        for (int i = 0; i < elementNames.length; i++) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(
                    "Trying to load " + elementNames[i] + ", prefix=" + prefix + ", expected type=" + expectedType);
            }

            elements[i] = initElement(config, prefix + "." + elementNames[i], expectedType, register);
        }

        return elements;
    }


    /**
     * Uses introspection and annotations to apply configuration parameters.
     *
     * @param object object to be configured.
     * @param config reference to Configuration object.
     * @param prefix id of the object.
     *
     * @throws ConversionException if unable to convert value from the configuration to a type expected by a getter
     *         method
     * @throws InitializationException if unable to initialize object due to some error, in most cases invalid
     *         property name.
     */
    public static void autoConfigure(Object object, Configuration config, String prefix)
        throws ConversionException {
        if (prefix == null) {
            LOGGER.warn("Unable to auto-configure object " + object + ", no identifier provided");

            return;
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Trying to auto-configure object identified by " + prefix);
        }

        PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(object.getClass());

        for (PropertyDescriptor descriptor : descriptors) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Descriptor for: " + descriptor.getName());
            }

            Method writeMethod = descriptor.getWriteMethod();

            if (writeMethod == null) {
                LOGGER.warn("Property " + descriptor.getName() + " can not be written for " + object.getClass());

                continue;
            }

            Class propType = descriptor.getPropertyType();

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Trying to set value of type " + propType + " to property " + descriptor.getName());
            }

            String mName = propType.isPrimitive() ? ("get" + StringUtils.capitalize(propType.getName())) : "getProperty";
            Method configGetMethod = MethodUtils.getAccessibleMethod(Configuration.class, mName, String.class);

            try {
                Object value = configGetMethod.invoke(config, prefix + "." + descriptor.getName());

                if (value != null) {
                    writeMethod.invoke(object, new Object[] { value });
                } else {
                    LOGGER.warn(
                        "There is no value for key " + prefix + "." + descriptor.getName()
                        + " stored in configuration");
                }
            } catch (IllegalArgumentException e) {
                throw new InitializationException(e.getMessage(), e);
            } catch (IllegalAccessException e) {
                throw new InitializationException(e.getMessage(), e);
            } catch (InvocationTargetException e) {
                throw new InitializationException(e.getMessage(), e);
            }
        }
    }


    /**
     * Store configuration object in specified file.
     *
     * @param config object to be stored.
     * @param fileName name of the target file.
     *
     * @throws ConfigurationException if any error occurs during the operation.
     */
    public static void storeConfig(Configuration config, String fileName)
        throws ConfigurationException {
        if (config instanceof AbstractHierarchicalFileConfiguration) {
            ((AbstractHierarchicalFileConfiguration)config).save(fileName);
        } else {
            try {
                PrintWriter writer = new PrintWriter(fileName);

                for (Iterator it = config.getKeys(); it.hasNext();) {
                    String key = String.valueOf(it.next());
                    writer.print(key + "=" + String.valueOf(config.getProperty(key)));
                }
            } catch (FileNotFoundException e) {
                LOGGER.error("Unable to create file " + fileName);
            }
        }
    }
}
